﻿//    Copyright 2010,2011 Himanshu Vasishth
//   
//    This file is part of SecretVault.
//
//    SecretVault is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    SecretVault is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with SecretVault.  If not, see <http://www.gnu.org/licenses/>.
//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Common;
using PasswordStoreProvider;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Diagnostics;
using PasswordStoreProvider.Exceptions;

namespace FileStoreProvider
{
    /// <summary>
    /// The store for the 
    /// </summary>
    public class FileStore : IVaultStore
    {
        // cache of LoginInfo objects used
        private IEnumerable<LoginInfo> loginInfos;

        public void Dispose()
        {
            // nothing to do
        }

        private static Stream SerializeStore(List<StorageLoginInfo> storageLoginInfos)
        {
            var stream = new MemoryStream();
            var formatter = new BinaryFormatter();
            formatter.Serialize(stream, storageLoginInfos);
            stream.Seek(0, SeekOrigin.Begin);
            return stream;
        }

        private static List<StorageLoginInfo> DeSerializeStore(Stream stream)
        {
            try
            {
                stream.Seek(0, SeekOrigin.Begin);
                var formatter = new BinaryFormatter();
                return (List<StorageLoginInfo>)formatter.Deserialize(stream);
            }
            catch (Exception)
            {
                // This should really not happen since we were just able to decrypt the stream
                // but deal with it anyway by returning an empty list
                return new List<StorageLoginInfo>();
            }
        }

        public void Open(string masterPassword, string storeUrl)
        {
            StoreLocation = storeUrl;
            if (!File.Exists(storeUrl))
            {
                // Create a file with the given password since one doesn't exist
                SaveToFile(masterPassword, new List<StorageLoginInfo>());
                loginInfos = new List<LoginInfo>();
            }
            else
            {
                // Open the store and populate the cache of loginInfo objects
                UpdateLoginInfoCache(ReadFromFile(masterPassword));
            }
        }

        private void SaveToFile(string masterPassword,
                                List<StorageLoginInfo> list)
        {
            using (var stream = EncryptionUtilities.GetEncryptedStream(
                                masterPassword,
                                SerializeStore(list)))
            {
                stream.Seek(0, SeekOrigin.Begin);

                using (var fileStream = File.OpenWrite(StoreLocation))
                {
                    var buffer = new byte[1024];
                    int count;
                    while (buffer.Length == (count = stream.Read(buffer, 0, buffer.Length)))
                    {
                        fileStream.Write(buffer, 0, buffer.Length);
                    }
                    fileStream.Write(buffer, 0, count);
                }
            }
        }

        /// <summary>
        /// Reads the file and returns a list of StorageLoginInfo objects. Performs the decryption
        /// </summary>
        private List<StorageLoginInfo> ReadFromFile(string masterPassword)
        {
            try
            {

                using (var fileStream = File.OpenRead(StoreLocation))
                using (var stream = EncryptionUtilities.DecryptStream(
                                            masterPassword,
                                            fileStream))
                {
                    return DeSerializeStore(stream);
                }
            }
            catch (Exception ex)
            {
                // TODO: Need to do better
                throw new InvalidMasterPasswordException(ex);
            }
        }

        private void UpdateLoginInfoCache(List<StorageLoginInfo> storageLoginInfos)
        {
            loginInfos = storageLoginInfos.Select(
                sli => new LoginInfo
                           {
                               Login = sli.Login,
                               Resource = sli.Resource
                           });
        }

        public void ChangeMasterPassword(string oldPassword, string newPassword)
        {
            SaveToFile(newPassword, ReadFromFile(oldPassword));
        }

        public string StoreLocation
        {
            get;
            private set;
        }

        public void StoreLoginInfo(string masterPassword, LoginInfo loginInfo, string password)
        {
            try
            {
                var storageInfoList = ReadFromFile(masterPassword);

                var storageLoginInfo = new StorageLoginInfo
                                           {
                                               Login = loginInfo.Login,
                                               Resource = loginInfo.Resource,
                                               Password = password
                                           };

                // Remove the loginInfo
                RemoveLoginInfoFromList(storageInfoList, loginInfo);

                // Now add the new one.
                storageInfoList.Add(storageLoginInfo);

                // Save to file
                SaveToFile(masterPassword, storageInfoList);

                // update the cache
                UpdateLoginInfoCache(storageInfoList);
            }
            catch (Exception ex)
            {
                throw new SetPasswordFailedException(ex);
            }
        }

        public void DeleteLoginInfo(string masterPassword, LoginInfo loginInfo)
        {
            try
            {
                var storageInfoList = ReadFromFile(masterPassword);

                // Remove the loginInfo
                RemoveLoginInfoFromList(storageInfoList, loginInfo);

                // Save to file
                SaveToFile(masterPassword, storageInfoList);

                // update the cache
                UpdateLoginInfoCache(storageInfoList);

            }
            catch (Exception ex)
            {
                // Eat everything for now
                throw new DeletePasswordFailedException(ex);
            }
        }

        /// <summary>
        /// Removes a StorageLoginInfo object from the list which matches the LoginInfo passed
        /// </summary>
        /// <param name="storageInfoList"></param>
        /// <param name="loginInfo"></param>
        private void RemoveLoginInfoFromList(List<StorageLoginInfo> storageInfoList, LoginInfo loginInfo)
        {
            // Find the StorageInfoList that matches
            var result = storageInfoList.Find(
                sli => loginInfo.Equals(
                    new LoginInfo
                        {
                            Login = sli.Login,
                            Resource = sli.Resource
                        }));
            if (result != null)
            {
                storageInfoList.Remove(result);
            }
        }

        public IEnumerable<LoginInfo> LoginInfos()
        {
            return loginInfos;
        }

        public string GetPassword(string masterPassword, LoginInfo loginInfo)
        {
            try
            {

                var storageInfoList = ReadFromFile(masterPassword);
                var result = storageInfoList.Find(
                    sli => loginInfo.Equals(
                        new LoginInfo
                            {
                                Login = sli.Login,
                                Resource = sli.Resource
                            }));
                return result.Password;
            }
            catch (Exception ex)
            {
                throw new PasswordNotFoundException(loginInfo, ex);
            }
        }

        public void UpdateLoginInfo(
                    string masterPassword,
                    LoginInfo loginInfo,
                    LoginInfo newLoginInfo,
                    string newPassword)
        {
            try
            {

                var storageInfoList = ReadFromFile(masterPassword);

                // Remove the old loginInfo
                RemoveLoginInfoFromList(storageInfoList, loginInfo);

                // Now add the new one.
                storageInfoList.Add(new StorageLoginInfo
                                        {
                                            Login = newLoginInfo.Login,
                                            Resource = newLoginInfo.Resource,
                                            Password = newPassword
                                        });

                // Save to file
                SaveToFile(masterPassword, storageInfoList);

                // update the cache
                UpdateLoginInfoCache(storageInfoList);
            }
            catch (Exception ex)
            {
                throw new SetPasswordFailedException(ex);
            }
        }
    }
}
